﻿/*
Copyright © 2010 François Karman

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

See <license.txt> for the complete license of the software.
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;

namespace MSBuild.Mercurial
{
    /// <summary>
    /// Manages and encapsulates the call the the <c>hg</c> command.
    /// </summary>
    internal class HgProcess : IDisposable
    {
        /// <summary>
        /// Defines the name of the Mercurial command (value: <c>hg.exe</c>).
        /// </summary>
        private const string MercurialCommand = "hg.exe";

        /// <summary>
        /// The location of the Mercurial <c>hg.exe</c> command.
        /// </summary>
        /// <remarks>
        /// If <c>null</c>, the command is considered as part of the
        /// default search path.
        /// </remarks>
        private readonly string libraryLocation;

        /// <summary>
        /// The executed command (the parameters for <c>hg.exe</c>).
        /// </summary>
        private readonly string command;

        /// <summary>
        /// The working directory for the created process.
        /// </summary>
        private readonly string workingDirectory;

        /// <summary>
        /// The encapsulated process.
        /// </summary>
        private Process process;

        /// <summary>
        /// The lines received on the standard output stream.
        /// </summary>
        private ICollection<string> outputs;

        /// <summary>
        /// The lines received on the standard error stream.
        /// </summary>
        private ICollection<string> errors;

        /// <summary>
        /// Initializes a new instance of the HgProcess class
        /// with a specific libraryLocation.
        /// </summary>
        /// <param name="libraryLocation">
        /// The location of the Mercurial <c>hg.exe</c> command.
        /// Can be <c>null</c>.
        /// </param>
        /// <param name="command">
        /// The executed command (the parameters for <c>hg.exe</c>).
        /// </param>
        /// <param name="workingDirectory">
        /// The working directory for the created process.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// One of the parameters <paramref name="command"/> or
        /// <paramref name="workingDirectory"/> is <c>null</c>.
        /// </exception>
        public HgProcess(string libraryLocation, string command, string workingDirectory)
        {
            if (command == null)
            {
                throw new ArgumentNullException("command");
            }

            if (workingDirectory == null)
            {
                throw new ArgumentNullException("workingDirectory");
            }

            this.libraryLocation = libraryLocation;
            this.command = command;
            this.workingDirectory = workingDirectory;

            this.outputs = new LinkedList<string>();
            this.errors = new LinkedList<string>();
        }

        /// <summary>
        /// Gets the location of the Mercurial <c>hg.exe</c> command.
        /// </summary>
        /// <remarks>
        /// If <c>null</c>, the command is considered as part of the
        /// default search path.
        /// </remarks>
        public string LibraryLocation
        {
            get { return this.libraryLocation; }
        }

        /// <summary>
        /// Gets the executed command (the parameters for <c>hg.exe</c>).
        /// </summary>
        public string Command
        {
            get { return this.command; }
        }

        /// <summary>
        /// Gets the working directory for the created process.
        /// </summary>
        public string WorkingDirectory
        {
            get { return this.workingDirectory; }
        }

        /// <summary>
        /// Gets the current status of the process.
        /// </summary>
        public HgProcessStatus Status { get; private set; }

        /// <summary>
        /// Gets the lines received on the standard output stream
        /// of the process.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// This property is used before the end of the process.
        /// </exception>
        public ICollection<string> Outputs
        {
            get
            {
                this.ValidStatus(HgProcessStatus.Ended);
                return this.outputs;
            }
        }

        /// <summary>
        /// Gets the lines received on the standard error stream
        /// of the process.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// This property is used before the end of the process.
        /// </exception>
        public ICollection<string> Errors
        {
            get
            {
                this.ValidStatus(HgProcessStatus.Ended);
                return this.errors;
            }
        }

        /// <summary>
        /// Starts the process.
        /// </summary>
        /// <exception cref="HgProcessException">
        /// The <c>hg.exe</c> command is not available.
        /// </exception>
        public void Start()
        {
            this.ValidStatus(HgProcessStatus.Created);

            // Create the full name of the command based on the
            // library location, if any.
            string processFileName = MercurialCommand;
            if (!string.IsNullOrEmpty(this.LibraryLocation))
            {
                processFileName = Path.Combine(this.LibraryLocation, processFileName);
            }

            // Initialize the info about the process.
            ProcessStartInfo startInfo = new ProcessStartInfo(processFileName, this.Command);
            startInfo.CreateNoWindow = true;
            startInfo.RedirectStandardError = true;
            startInfo.RedirectStandardOutput = true;
            startInfo.UseShellExecute = false;
            startInfo.WorkingDirectory = this.WorkingDirectory;

            // Initialize the process
            this.process = new Process();
            this.process.StartInfo = startInfo;
            this.process.OutputDataReceived += new DataReceivedEventHandler(this.OnOutputDataReceived);
            this.process.ErrorDataReceived += new DataReceivedEventHandler(this.OnErrorDataReceived);

            try
            {
                this.process.Start();
                this.Status = HgProcessStatus.Started;
            }
            catch (Win32Exception exception)
            {
                this.process = null;
                this.Status = HgProcessStatus.Ended;
                string message = string.Format(Error.Culture, Error.LibraryNotAvailable, MercurialCommand);
                throw new HgProcessException(message, exception);
            }

            this.process.BeginOutputReadLine();
            this.process.BeginErrorReadLine();
        }

        /// <summary>
        /// Waits for the end of the process.
        /// </summary>
        /// <param name="timeout">
        /// The maximum time for the process to exit - in milliseconds.
        /// If <c>0</c>, wait indefinitely for the end of the process.
        /// </param>
        /// <exception cref="HgProcessException">
        /// The process doesn't end before <paramref name="timeout"/>.
        /// </exception>
        public void Wait(int timeout)
        {
            this.ValidStatus(HgProcessStatus.Started);

            try
            {
                if (timeout != 0)
                {
                    if (!this.process.WaitForExit(timeout))
                    {
                        this.process.Close();

                        string message = string.Format(Error.Culture, Error.ProcessTimeout, timeout);
                        throw new HgProcessException(message);
                    }
                }

                // WaitForExit() is used in two situations:
                // - timeout == 0
                // - after WaitForExit(x) to ensure that the async outputs are closed.
                this.process.WaitForExit();
            }
            finally
            {
                this.process = null;
                this.Status = HgProcessStatus.Ended;
            }
        }

        /// <summary>
        /// Starts the process and wait for its end.
        /// </summary>
        /// <param name="timeout">
        /// The timeout of the process - in milliseconds.
        /// If <c>0</c>, wait indefinitely for the end of the process.
        /// </param>
        /// <exception cref="HgProcessException">
        /// A critical error occurs during the process.
        /// </exception>
        public void StartAndWait(int timeout)
        {
            this.Start();
            this.Wait(timeout);
        }

        /// <summary>
        /// Releases the encapsulated process.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Valids the current status of the process.
        /// </summary>
        /// <param name="expected">
        /// The expected status.
        /// </param>
        /// <exception cref="InvalidOperationException">
        /// The current status is not conform to the expected one.
        /// </exception>
        private void ValidStatus(HgProcessStatus expected)
        {
            if (expected != this.Status)
            {
                string message = string.Format(Error.Culture, Error.ProcessInvalidState, this.Status, expected);
                throw new InvalidOperationException(message);
            }
        }

        /// <summary>
        /// Releases the encapsulated process.
        /// </summary>
        /// <param name="disposing">
        /// A value indicating whether the managed resource should
        /// be released.
        /// </param>
        /// <remarks>
        /// The encapsulated process is considered as a
        /// managed resource.
        /// </remarks>
        private void Dispose(bool disposing)
        {
            if (disposing == true)
            {
                if (this.process != null)
                {
                    this.process.Dispose();
                }
            }
        }

        /// <summary>
        /// Called when the associated process send a message
        /// to its standard output stream.
        /// </summary>
        /// <param name="sender">
        /// This parameter is not used.
        /// </param>
        /// <param name="e">
        /// The message send to the standard stream.
        /// </param>
        private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data != null)
            {
                lock (this.outputs)
                {
                    this.outputs.Add(e.Data);
                }
            }
        }

        /// <summary>
        /// Called when the associated process send a message
        /// to its standard error stream.
        /// </summary>
        /// <param name="sender">
        /// This parameter is not used.
        /// </param>
        /// <param name="e">
        /// The message send to the standard stream.
        /// </param>
        private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data != null)
            {
                lock (this.errors)
                {
                    this.errors.Add(e.Data);
                }
            }
        }
    }
}
